Treating a Function Like an Object


In [11]:
def factorial(n):
    '''returns n!'''
    return 1 if n < 2 else n * factorial(n-1)

In [12]:
factorial(42)


Out[12]:
1405006117752879898543142606244511569936384000000000

In [13]:
factorial.__doc__


Out[13]:
'returns n!'

In [14]:
type(factorial)


Out[14]:
function

In [15]:
help(factorial)


Help on function factorial in module __main__:

factorial(n)
    returns n!


In [16]:
fact = factorial
fact


Out[16]:
<function __main__.factorial>

In [17]:
fact(5)


Out[17]:
120

In [18]:
map(factorial, range(11))


Out[18]:
<map at 0xcf039d7630>

In [19]:
list(map(factorial, range(11)))


Out[19]:
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]

Higher-Order Functions


In [20]:
fruits = [' strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
sorted(fruits, key=len)


Out[20]:
['fig', 'apple', 'cherry', 'banana', 'raspberry', ' strawberry']

In [22]:
def reverse(word):
    return word[::-1]
reverse('testing')


Out[22]:
'gnitset'

In [23]:
sorted(fruits, key=reverse)


Out[23]:
['banana', 'apple', 'fig', 'raspberry', ' strawberry', 'cherry']

Modern Replacement for map, filter, and reduce


In [24]:
list(map(fact, range(6)))


Out[24]:
[1, 1, 2, 6, 24, 120]

In [25]:
[fact(n) for n in range(6)]


Out[25]:
[1, 1, 2, 6, 24, 120]

In [26]:
list(map(factorial, filter(lambda n: n % 2, range(6))))


Out[26]:
[1, 6, 120]

In [27]:
[factorial(n) for n in range(6) if n % 2]


Out[27]:
[1, 6, 120]

In [28]:
from functools import reduce
from operator import add

In [29]:
reduce(add, range(100))


Out[29]:
4950

In [30]:
sum(range(100))


Out[30]:
4950

Anonymous Functions


In [31]:
fruits = [' strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']

In [32]:
sorted(fruits, key=lambda word: word[::-1])


Out[32]:
['banana', 'apple', 'fig', 'raspberry', ' strawberry', 'cherry']

User-Defined Callable Types


In [33]:
import random

In [35]:
class BingoCage:
    
    def __init__(self, items):
        self._items = list(items)
        random.shuffle(self._items)
        
    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
                raise LookupError('pick from empty BingoCage')
                
    def __call__(self):
        return self.pick()

In [36]:
bingo = BingoCage(range(3))
bingo.pick()


Out[36]:
2

In [39]:
bingo()


Out[39]:
1

In [40]:
callable(bingo)


Out[40]:
True

Function Introspection


In [41]:
dir(factorial)


Out[41]:
['__annotations__',
 '__call__',
 '__class__',
 '__closure__',
 '__code__',
 '__defaults__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__get__',
 '__getattribute__',
 '__globals__',
 '__gt__',
 '__hash__',
 '__init__',
 '__kwdefaults__',
 '__le__',
 '__lt__',
 '__module__',
 '__name__',
 '__ne__',
 '__new__',
 '__qualname__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__']

In [51]:
def upper_case_name(obj):
    return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = 'Customer name'

In [57]:
upper_case_name.__dict__


Out[57]:
{'short_description': 'Customer name'}

In [58]:
class C: pass
obj = C()
def func(): pass
sorted(set(dir(func)) - set(dir(obj)))


Out[58]:
['__annotations__',
 '__call__',
 '__closure__',
 '__code__',
 '__defaults__',
 '__get__',
 '__globals__',
 '__kwdefaults__',
 '__name__',
 '__qualname__']

From Positional to Keyword-Only Parameters


In [44]:
def tag(name, *content, cls=None, **attrs):
    """Generate one or more HTML tags"""
    if cls is not None:
        attrs['class'] = cls
    if attrs:
        attr_str = ''.join(' %s="%s"' % (attr,value)
                          for attr, value
                          in sorted(attrs.items()))
    else:
        attr_str = ''
    if content:
        return '\n'.join('<%s%s>%s</%s>' %
                         (name, attr_str, c, name) for c in content)
    else:
        return '<%s%s />' % (name, attr_str)

In [2]:
tag('br')


Out[2]:
'<br />'

In [3]:
tag('p', 'hello')


Out[3]:
'<p>hello</p>'

In [6]:
print(tag('p', 'hello', 'world'))


<p>hello</p>
<p>world</p>

In [7]:
tag('p', 'hello', id=33)


Out[7]:
'<p id="33">hello</p>'

In [8]:
print(tag('p', 'hello', 'world', cls='sidebar'))


<p class="sidebar">hello</p>
<p class="sidebar">world</p>

In [9]:
tag(content='testing', name="img")


Out[9]:
'<img content="testing" />'

In [14]:
my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
         'src': 'sunset.jpg', 'cls': 'framed'}

In [18]:
tag(**my_tag)


Out[18]:
'<img class="framed" src="sunset.jpg" title="Sunset Boulevard" />'

In [24]:
tag(name="img", 'testing')


  File "<ipython-input-24-f039e3570ddf>", line 1
    tag(name="img", 'testing')
                   ^
SyntaxError: positional argument follows keyword argument

In [31]:
def f(a, *, b):
    return a, b

In [34]:
f(1, b=3)


Out[34]:
(1, 3)

Retrieving Information About Parameters


In [36]:
import bobo

In [37]:
@bobo.query('/')
def hello(person):
    return 'Hello %s!' % person

In [47]:
tag.__code__


Out[47]:
<code object tag at 0x000000015ADDB810, file "<ipython-input-44-881dc1a0073b>", line 1>

In [94]:
def clip(text, max_len=80):
    """Return text clipped at the last space before or after max_len"""
    end = None
    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)
        if space_before >= 0:
            end = space_before
        else:
            space_after = text.rfind(' ', max_len)
            if space_after >= 0:
                end = space_after
    if end is None:
        end = len(text)
    return text[:end].rstrip()

In [50]:
clip.__defaults__


Out[50]:
(80,)

In [51]:
clip.__code__


Out[51]:
<code object clip at 0x000000015BF199C0, file "<ipython-input-48-b37f915ce8c1>", line 1>

In [52]:
clip.__code__.co_varnames


Out[52]:
('text', 'max_len', 'end', 'space_before', 'space_after')

In [53]:
clip.__code__.co_argcount


Out[53]:
2

In [97]:
from inspect import signature

In [98]:
sig = signature(clip)

In [99]:
sig


Out[99]:
<Signature (text, max_len=80)>

In [100]:
str(sig)


Out[100]:
'(text, max_len=80)'

In [101]:
for name, param in sig.parameters.items():
    print(param.kind, ':', name, '=', param.default)


POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80

In [102]:
import inspect

In [103]:
sig = inspect.signature(tag)

In [104]:
my_tag = {'name': 'img', 'title': 'Sunset Boulevard',
         'src': 'sunset.jpg', 'cls': 'framed'}

In [105]:
bound_args = sig.bind(**my_tag)

In [106]:
bound_args


Out[106]:
<BoundArguments (name='img', cls='framed', attrs={'src': 'sunset.jpg', 'title': 'Sunset Boulevard'})>

In [107]:
for name, value in bound_args.arguments.items():
    print(name, '=', value)


name = img
cls = framed
attrs = {'src': 'sunset.jpg', 'title': 'Sunset Boulevard'}

In [108]:
del my_tag['name']

In [109]:
bound_args = sig.bind(**my_tag)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-109-1e763c041430> in <module>()
----> 1 bound_args = sig.bind(**my_tag)

c:\users\langestrst01\appdata\local\continuum\anaconda3\envs\fluentpy\lib\inspect.py in bind(*args, **kwargs)
   2917         if the passed arguments can not be bound.
   2918         """
-> 2919         return args[0]._bind(args[1:], kwargs)
   2920 
   2921     def bind_partial(*args, **kwargs):

c:\users\langestrst01\appdata\local\continuum\anaconda3\envs\fluentpy\lib\inspect.py in _bind(self, args, kwargs, partial)
   2832                             msg = 'missing a required argument: {arg!r}'
   2833                             msg = msg.format(arg=param.name)
-> 2834                             raise TypeError(msg) from None
   2835             else:
   2836                 # We have a positional argument to process

TypeError: missing a required argument: 'name'

Function Annotations


In [2]:
def clip(text:str, max_len:'int > 0'=80) -> str:
    """Return text clipped at the last space before or after max_len"""
    end = None
    if len(text) > max_len:
        space_before = text.rfind(' ', 0, max_len)
        if space_before >= 0:
            end = space_before
        else:
            space_after = text.rfind(' ', max_len)
            if space_after >= 0:
                end = space_after
    if end is None:
        end = len(text)
    return text[:end].rstrip()

In [3]:
clip.__annotations__


Out[3]:
{'max_len': 'int > 0', 'return': str, 'text': str}

In [4]:
from inspect import signature

In [5]:
sig = signature(clip)
sig.return_annotation


Out[5]:
str

In [6]:
for param in sig.parameters.values():
    note = repr(param.annotation).ljust(13)
    print(note, ':', param.name, '=', param.default)


<class 'str'> : text = <class 'inspect._empty'>
'int > 0'     : max_len = 80

Packages for Functional Programming

The Operator Module


In [9]:
from functools import reduce

In [10]:
def fact(n):
    return reduce(lambda a, b: a*b, range(1, n+1))

In [11]:
from operator import mul

In [12]:
def fact(n):
    return reduce(mul, range(1, n+1))

In [13]:
metro_data = [
    ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
    ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
    ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
    ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
    ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]

In [14]:
from operator import itemgetter

In [15]:
for city in sorted(metro_data, key=itemgetter(1)):
    print(city)


('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889))
('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
('Mexico City', 'MX', 20.142, (19.433333, -99.133333))
('New York-Newark', 'US', 20.104, (40.808611, -74.020386))

In [16]:
cc_name = itemgetter(1, 0)
for city in metro_data:
    print(cc_name(city))


('JP', 'Tokyo')
('IN', 'Delhi NCR')
('MX', 'Mexico City')
('US', 'New York-Newark')
('BR', 'Sao Paulo')

In [17]:
from collections import namedtuple

In [18]:
LatLong = namedtuple('LatLong', 'lat long')
Metropolis = namedtuple('Metropolis', 'name cc pop coord')
metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long))
              for name, cc, pop, (lat, long) in metro_data]
metro_areas[0]


Out[18]:
Metropolis(name='Tokyo', cc='JP', pop=36.933, coord=LatLong(lat=35.689722, long=139.691667))

In [19]:
metro_areas[0].coord.lat


Out[19]:
35.689722

In [20]:
from operator import attrgetter

In [21]:
name_lat = attrgetter('name', 'coord.lat')

In [22]:
for city in sorted(metro_areas, key = attrgetter('coord.lat')):
    print(name_lat(city))


('Sao Paulo', -23.547778)
('Mexico City', 19.433333)
('Delhi NCR', 28.613889)
('Tokyo', 35.689722)
('New York-Newark', 40.808611)

In [28]:
import operator
print([name for name in dir(operator) if not name.startswith('_')])


['abs', 'add', 'and_', 'attrgetter', 'concat', 'contains', 'countOf', 'delitem', 'eq', 'floordiv', 'ge', 'getitem', 'gt', 'iadd', 'iand', 'iconcat', 'ifloordiv', 'ilshift', 'imatmul', 'imod', 'imul', 'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irshift', 'is_', 'is_not', 'isub', 'itemgetter', 'itruediv', 'ixor', 'le', 'length_hint', 'lshift', 'lt', 'matmul', 'methodcaller', 'mod', 'mul', 'ne', 'neg', 'not_', 'or_', 'pos', 'pow', 'rshift', 'setitem', 'sub', 'truediv', 'truth', 'xor']

In [29]:
from operator import methodcaller

In [30]:
s = 'The time has come'
upcase = methodcaller('upper')
upcase(s)


Out[30]:
'THE TIME HAS COME'

In [32]:
hiphenate = methodcaller('replace', ' ', '-')
hiphenate(s)


Out[32]:
'The-time-has-come'

Freezing Arguments with functools, partial


In [33]:
from operator import mul
from functools import partial

In [34]:
triple = partial(mul, 3)
triple(7)


Out[34]:
21

In [35]:
list(map(triple, range(1, 10)))


Out[35]:
[3, 6, 9, 12, 15, 18, 21, 24, 27]

In [36]:
import unicodedata, functools

In [37]:
nfc = functools.partial(unicodedata.normalize, 'NFC')

In [38]:
s1 = 'café'
s2 = 'cafe\u0301'

In [39]:
s1, s2


Out[39]:
('café', 'café')

In [40]:
s1 == s2


Out[40]:
False

In [41]:
nfc(s1) == nfc(s2)


Out[41]:
True

In [42]:
def tag(name, *content, cls=None, **attrs):
    """Generate one or more HTML tags"""
    if cls is not None:
        attrs['class'] = cls
    if attrs:
        attr_str = ''.join(' %s="%s"' % (attr,value)
                          for attr, value
                          in sorted(attrs.items()))
    else:
        attr_str = ''
    if content:
        return '\n'.join('<%s%s>%s</%s>' %
                         (name, attr_str, c, name) for c in content)
    else:
        return '<%s%s />' % (name, attr_str)

In [43]:
tag


Out[43]:
<function __main__.tag>

In [44]:
from functools import partial

In [45]:
picture = partial(tag, 'img', cls='pic-frame')
picture(src='wumpus.jpeg')


Out[45]:
'<img class="pic-frame" src="wumpus.jpeg" />'

In [46]:
picture


Out[46]:
functools.partial(<function tag at 0x00000090503E5730>, 'img', cls='pic-frame')

In [47]:
picture.func


Out[47]:
<function __main__.tag>

In [48]:
picture.args


Out[48]:
('img',)

In [49]:
picture.keywords


Out[49]:
{'cls': 'pic-frame'}

In [ ]: